package ph.com.gs3.formalistics.service.managers;

import io.socket.IOAcknowledge;
import io.socket.IOCallback;
import io.socket.SocketIO;
import io.socket.SocketIOException;

import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;

import ph.com.gs3.formalistics.global.utilities.logging.FLLogger;
import ph.com.gs3.formalistics.model.valueobjects.business.User;
import ph.com.gs3.formalistics.model.valueobjects.business.UserStub;

public class SocketManager {

	// ===============================================================================
	// {{ Constants
	public static final String TAG = SocketManager.class.getSimpleName();

	// socket broadcast keys
	public static final String SOCKET_KEY_REGISTER_USER = "register";
	public static final String SOCKET_KEY_MESSAGE = "message";

	// events (derived from formalistics socket server)
	public static final String EVENT_USER_LOGGED_IN = "userLoggedIn";
	public static final String EVENT_USER_LOGGED_OUT = "userLoggedOut";
	public static final String EVENT_MESSAGE_RECEIVED = "messageRecieved";

	// other constants
	public static final int RECONNECT_TRY_COUNT = 3;

	// }}

	// ===============================================================================
	// {{ Default Instance

	private static SocketManager defaultInstance;

	public static SocketManager getDefaultInstance() {
		if (defaultInstance == null) {
			defaultInstance = new SocketManager();
		}

		return defaultInstance;
	}

	// }}

	// ===============================================================================
	// {{ Fields & Flags

	private static int reconnectTryCount = 0;

	private boolean isConnected = false;

	private int socketServerPort;
	private User currentUser;

	private SocketIO socket = null;

	private List<SocketManagerListener> listeners;

	// }}

	public SocketManager() {
		listeners = new ArrayList<>();
	}

	// ===============================================================================
	// {{ Notify & Listener Registration Methods

	public synchronized void registerListner(SocketManagerListener listener) {
		listeners.add(listener);
	}

	public synchronized void unregisterListener(SocketManagerListener listener) {
		listeners.remove(listener);
	}

	private synchronized void notifyOnConnect() {

		for (SocketManagerListener listener : listeners) {
			listener.onConnected();
		}

	}

	private synchronized void notifyOnDisconnect() {
		for (SocketManagerListener listener : listeners) {
			listener.onDisconnected();
		}
	}

	private synchronized void notifyOnUserLoggedIn(String userRawJSONString) {

		UserStub user = getUserFromSocketUserJSONString(userRawJSONString);

		for (SocketManagerListener listener : listeners) {
			listener.onUserLoggedIn(user);
		}
	}

	private synchronized void notifyOnUserLoggedOut(String userRawJSONString) {

		UserStub user = getUserFromSocketUserJSONString(userRawJSONString);

		for (SocketManagerListener listener : listeners) {
			listener.onUserLoggedOut(user);
		}
	}

	// ===============================================================================
	// {{ Functional Methods

	public void connect(User currentUser, int socketServerPort) {
		FLLogger.d(TAG, "trying to connect");
		if (!isConnected) {
			initializeSocketServer(currentUser, socketServerPort);
			socket.connect(ioCallbackImpl);
			registerUserToSocketServer(currentUser);
			isConnected = true;
		} else {
			FLLogger.w(
			        TAG,
			        "A client code tried to connect to the server while the manager is still connected. It's command is ignored");
		}

	}

	public void disconnect() {
		FLLogger.d(TAG, "now trying to disconnect");
		if (isConnected) {
			// this causes a leak in the application
			socket.disconnect();
			// socket = null;
			isConnected = false;
		}
	}

	public void sendMessage(String recipientId, String message) {

		JSONObject messageAuthorJSON = new JSONObject();
		JSONObject messageDataJSON = new JSONObject();
		try {

			messageAuthorJSON.put("id", currentUser.getId());
			messageAuthorJSON.put("displayName", currentUser.getDisplayName());
			messageAuthorJSON.put("imageURL", currentUser.getImageURL());

			messageDataJSON.put("sendTo", recipientId);
			messageDataJSON.put("from", messageAuthorJSON);
			messageDataJSON.put("message", message);

			socket.emit(SOCKET_KEY_MESSAGE, messageDataJSON);
		} catch (JSONException e) {
			e.printStackTrace();
		}

	}

	private void registerUserToSocketServer(User user) {

		JSONObject messageJSON = new JSONObject();
		try {
			messageJSON.put("userData", User.createSocketJSONFromUser(user));
			socket.emit(SOCKET_KEY_REGISTER_USER, messageJSON);
		} catch (JSONException e) {
			e.printStackTrace();
		}

	}

	// }}

	// ===============================================================================
	// {{ Private Utility Methods

	private void initializeSocketServer(User user, int socketPort) {
		String fullServer = user.getAccountServer() + ":" + socketPort;
		try {
			socket = new SocketIO(fullServer);

			this.currentUser = user;
			this.socketServerPort = socketPort;
		} catch (MalformedURLException e) {
			e.printStackTrace();
			throw new IllegalArgumentException("The server's URL is malformed", e);
		}
	}

	private UserStub getUserFromSocketUserJSONString(String userRawJSONString) {

		try {
			JSONObject userRawJSON = new JSONObject(userRawJSONString);

			FLLogger.d(TAG, "Received data: " + userRawJSON.toString());

			// parse the JSON to user
			UserStub user = new UserStub();
			user.setId(userRawJSON.getString("id"));
			user.setDisplayName(userRawJSON.getString("displayName"));
			user.setImageURL(userRawJSON.getString("imageURL"));

			return user;
		} catch (JSONException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return null;

	}

	// }}

	public IOCallback ioCallbackImpl = new IOCallback() {

		@Override
		public void onMessage(JSONObject json, IOAcknowledge ack) {
			FLLogger.d(TAG, "message: " + json.toString());
		}

		@Override
		public void onMessage(String data, IOAcknowledge ack) {
			FLLogger.d(TAG, "message: " + data);
		}

		@Override
		public void onError(SocketIOException socketIOException) {

			socketIOException.printStackTrace();

			// socketIOException.printStackTrace();
			FLLogger.e(TAG, socketIOException.getMessage());

			if (reconnectTryCount < 3) {
				// try to reconnect
				disconnect();
				connect(currentUser, socketServerPort);

				reconnectTryCount++;
			} else {
				FLLogger.w(TAG, "Reconnect try limit exceeded. Handle this error properly later.");
			}

		}

		@Override
		public void onDisconnect() {
			// System.out.println("Connection terminated.");
			FLLogger.d(TAG, "Connection terminated");
			reconnectTryCount = 0;

			// if (isConnected) {
			// FLLogger.d(TAG, "Trying to reconnect");
			// // try to reconnect
			// isConnected = false;
			// connect();
			// FLLogger.d(TAG, "Reconnected");
			// }

			notifyOnDisconnect();

		}

		@Override
		public void onConnect() {
			// listener.onConnected();
			FLLogger.d(TAG, "connected to socket io");
			reconnectTryCount = 0;

			notifyOnConnect();
		}

		@Override
		public void on(String event, IOAcknowledge ack, Object... args) {
			FLLogger.d(TAG, "event: " + event);

			String dataString;

			try {
				dataString = args[0].toString();
			} catch (NullPointerException ex) {

				return;
			}

			if (EVENT_USER_LOGGED_IN.equals(event)) {
				notifyOnUserLoggedIn(dataString);
			}

			if (EVENT_USER_LOGGED_OUT.equals(event)) {
				notifyOnUserLoggedOut(dataString);
			}

			// if (EVENT_MESSAGE_RECEIVED.equals(event)) {
			// notifyOnMessage(dataString);
			// }

		}
	};

	public static interface SocketManagerListener {

		public void onConnected();

		public void onRegistered();

		public void onDisconnected();

		public void onUserLoggedIn(UserStub user);

		public void onUserLoggedOut(UserStub user);

	}

}
